import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

class Handler extends Thread {
    Socket _sock = null;
    private TcpServer _server = null;

    public Handler(TcpServer server, Socket sock) {
        _server = server;
        _sock = sock;
        return;
    }

    private void readSockInputStreamMessage() {
        if (_sock != null) {
            try {
                if (_sock.getInputStream() != null) {
                    int len = _sock.getInputStream().available();
                    if (0 < len) {
                        System.out.println("client [" + _sock.getRemoteSocketAddress() + "] receive message length (" + len + ").");
                        byte[] data = new byte[len];
                        _sock.getInputStream().read(data);
                        String message = new String(data, StandardCharsets.UTF_8);

                        if (_server != null) {
                            _server._messge.onRecvMessage(_server, _sock, message);
                        }
                    } else {
                        //System.out.println("data inputstream is not available.");
                        Thread.sleep(100);
                    }
                } else {
                    //System.out.println("socket inputstream instance is null.");
                    Thread.sleep(100);
                }
            } catch (IOException e) {
                //e.printStackTrace();
                System.out.println(e.getMessage());
                Thread.currentThread().interrupt();
            } catch (InterruptedException e) {
                //e.printStackTrace();
                System.out.println(e.getMessage());
            }
        } else {
            System.out.println("socket instance is null.");
        }

        return;
    }

    @Override
    public void run() {
        if (_sock != null) {
            System.out.println("client [" + _sock.getRemoteSocketAddress() + "] read message thread is running......");

            try {
                while (/*_sock.isConnected()*/0 <= _sock.getInputStream().read()) {
                    // when _sock.getInputStream().read() < 0, it is closed by remote socket.
                    readSockInputStreamMessage();
                }
            } catch (IOException e) {
                System.out.println(e.getMessage());
                try {
                    _sock.close();
                    _sock = null;
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
                Thread.currentThread().interrupt();
            }

            System.out.println("client [" + _sock.getRemoteSocketAddress() + "] is disconnected.");
        }
        return;
    }
}

public class TcpServer {
    public interface Message{
        void onRecvMessage(TcpServer server, Socket sock, String message);
    }

    private class AcceptRunnable implements Runnable{
        private TcpServer _server = null;

        public AcceptRunnable(TcpServer server) {
            _server = server;
            return;
        }

        @Override
        public void run() {
            if (_server != null) {
                System.out.println("server is running on port: " + _server.getPort() + ".");
                while (_server.isStarted()) {
                    try {
                        Socket sock = _server.getServerSocket().accept();
                        sock.setKeepAlive(true);
                        System.out.println("client from " + sock.getRemoteSocketAddress() + " connected.");
                        new Handler(_server, sock).start();
                    } catch (IOException e) {
                        System.out.println(e.getMessage());
                    }
                }
            }
            return;
        }
    }

    private volatile boolean _started = false;
    private int _port = 0;
    private ServerSocket _serverSocket = null;

    public Message _messge = null;

    public void setMessageCallback(Message message) {
        _messge = message;
        return;
    }

    public boolean isStarted() {
        return _started;
    }

    public int getPort() {
        return _port;
    }

    public ServerSocket getServerSocket() {
        return _serverSocket;
    }

    public boolean start(String ip, int port, int maxConnections) {
        if (!_started) {
            _port = port;
            try {
                maxConnections = maxConnections < 1 ? 50 : maxConnections;
                //InetAddress addr = Inet4Address.getByAddress(new byte[]{127,0,0,1});
                _serverSocket = new ServerSocket(_port, maxConnections, InetAddress.getByName(ip));
                _started = true;
                new Thread(new AcceptRunnable(this)).start();
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
        }

        return _started;
    }

    public boolean quickStart(int port) {
        if (!_started) {
            _port = port;
            try {
                _serverSocket = new ServerSocket(_port);
                _started = true;
                /*new Thread(()->{
                    try {
                        System.out.println("server is running on port: " + this._port + ".");
                        while (this._started) {
                            Socket sock = this._serverSocket.accept();
                            System.out.println("client from " + sock.getRemoteSocketAddress() + " connected.");
                        }
                    } catch (IOException e) {
                        System.out.println(e.getMessage());
                    }
                }).start();*/
                new Thread(new AcceptRunnable(this)).start();
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
        }
        return _started;
    }

    public void stop() {
        if (_started) {
            _started = false;
            try {
                if (_serverSocket != null) {
                    _serverSocket.close();
                    _serverSocket = null;
                }
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
        }
        return;
    }

    public void writeStringMessage(Socket sock, String message) {
        if (sock != null && message != null) {
            if (_started && sock.isConnected()) {
                try {
                    if (sock.getOutputStream() != null) {
                        sock.getOutputStream().write(message.getBytes());
                        sock.getOutputStream().flush();
                    }
                } catch (IOException e) {
                    System.out.println(e.getMessage());
                }
            }
        }
        return;
    }

    public void writeBytesMessage(Socket sock, byte[] message) {
        if (sock != null && message != null) {
            if (_started && sock.isConnected()) {
                try {
                    if (sock.getOutputStream() != null) {
                        sock.getOutputStream().write(message);
                        sock.getOutputStream().flush();
                    }
                } catch (IOException e) {
                    System.out.println(e.getMessage());
                }
            }
        }

        return;
    }
}